/*******************************************************************************
 * Copyright 2014 United States Government as represented by the
 * Administrator of the National Aeronautics and Space Administration.
 * All Rights Reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package gov.nasa.ensemble.common.id;

import gov.nasa.ensemble.common.logging.LogUtil;
import gov.nasa.ensemble.common.mission.MissionExtendable;

import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.UUID;

public class UniqueIdGenerator implements MissionExtendable {

	/** The singleton instance, lazily created. */
	protected static UniqueIdGenerator INSTANCE = null;
	
	private static int nextSequentialSuffix = 1;
	
	protected /* almost final */ static long SESSION_ID_PREFIX = generateUniqueIdNumber();
	
	/** Generate a completely unique and unguessable id each time. */
	public static long generateUniqueIdNumber() {
		UUID randomUUID = UUID.randomUUID();
		long source1a = randomUUID.getMostSignificantBits();
		long source1b = randomUUID.getLeastSignificantBits();
		UUID jvmUUID = UUID.nameUUIDFromBytes(ManagementFactory.getOperatingSystemMXBean().getName().getBytes());
		long source2a = jvmUUID.getMostSignificantBits();
		long source2b = jvmUUID.getLeastSignificantBits();
		long source3 = System.nanoTime();
		long xor = source1a ^ source1b ^ source2a ^ source2b ^ source3;
		return xor < 0? ~xor : xor;
	}
	
	/** 
	 * Generate an id with a prefix that is unique but stays the same throughout the run,
	 * with a sequentially increasing counter at the end which is guaranteed never to repeat
	 * during the run because it's synchronized.  If it gets large enough to spill over the
	 * allocated length, it doesn't wrap around, but starts eating into (carried the 1 into)
	 * the end of the prefix.
	 * <p>
	 * Rationale:  <ol>
	 * <li> Unlike with cookie applications, a plan file is not trying to
	 * make the next id unguessable or hide its source.  On the contrary, it may be useful
	 * for troubleshooting to see what was generated by one run of, say, Score and in what order.
	 * <li> If the need arises to manually compare id's, it focuses attention on the part
	 * that is known to change.
	 * @param desiredLength -- number of characters to return (limited by the number of bits in a long over the log base 2 of the radix)
	 * @param nDigitsReservedForCounter -- not exactly "reserved"; can split over; e.g. calling generateSequentiallyIncreasingIdNumber(6, 3, 10) a might yield "742001", "742002", ..., "742998", "742999", "743000" 
	 * @param radix -- 10 for decimal, 16 for hexadecimal, 36 for base 36, etc.
	 * @return
	 */
	public String generateSequentiallyIncreasingIdNumber(int desiredLength, int nDigitsReservedForCounter, int radix) {
		return generateSequentiallyIncreasingIdNumber(SESSION_ID_PREFIX, desiredLength, nDigitsReservedForCounter, radix, false);
	}

	/**
	 * @see generateSequentiallyIncreasingIdNumber(int, int, int)}
	 */
	public String generateSequentiallyIncreasingIdNumber(long sessionIdPrefix, int desiredLength, int nDigitsReservedForCounter, int radix,
			boolean alreadyHandlingUnlikelyEdgeCase) {
		final int MAX_BITS = Long.SIZE-1;
		int lengthAvailable = (int) Math.floor(MAX_BITS / (Math.log(radix)/Math.log(2)));
		if (lengthAvailable < desiredLength) {
			throw new IllegalArgumentException("The maximum id length implemented for radix " +
					+ radix + " currently is " + lengthAvailable);
		}
		int desiredPrefixLength = desiredLength - nDigitsReservedForCounter;
		long multiplier = (long)Math.pow(radix, nDigitsReservedForCounter);
		long prefix = sessionIdPrefix % (long) Math.pow(radix, desiredPrefixLength);
		if (prefix < Math.pow(radix, desiredPrefixLength-1)) {
			prefix += Math.pow(radix, desiredPrefixLength-1);
		}
		long counter = generateNextSequentialCounter();
		long id = prefix * multiplier + counter;
		if (id < 0) {
			return handleUnlikelyEdgeCase(id, counter, sessionIdPrefix, desiredLength, nDigitsReservedForCounter, radix,
					alreadyHandlingUnlikelyEdgeCase);
		}
		String proposedResult = Long.toString(id, radix);
		if (proposedResult.length() != desiredLength) {
			return handleUnlikelyEdgeCase(id, counter, sessionIdPrefix, desiredLength, nDigitsReservedForCounter, radix,
					alreadyHandlingUnlikelyEdgeCase);
		}
		return proposedResult;
	}

	/** This is astronomically unlikely with sufficiently long id's and not incredible numbers of them,
	 * since it can only happen when the counter overflows into a random prefix that is also so large
	 * it may overflow.  As an example, consider the case where radix is 10, desiredLength is only 5,
	 * with 3 digits reserved for the counter.  Then it has a 1% chance of choosing 99 as sessionIdPrefix.
	 * In that case, the 999th call will generate 99,999 and the thousandth will generate 100,000,
	 * exceeding the desiredLength.  
	 * 
	 *  @return fallback id
	 */
	protected String handleUnlikelyEdgeCase(long badId, long counter, long sessionIdPrefix,
			int desiredLength, int nDigitsReservedForCounter, int radix,
			boolean alreadyHandlingUnlikelyEdgeCase) {
		long newSessionIdPrefix = new Random(System.nanoTime()).nextLong();
		if (newSessionIdPrefix < 0) {
			newSessionIdPrefix /= -2;
		}
		String parameterDescription = "counter=" + Long.toString(counter, radix) + 
				", sessionIdPrefix=" + Long.toString(sessionIdPrefix, radix) +
				", desiredLength=" + desiredLength +
				", nDigitsReservedForCounter=" + nDigitsReservedForCounter;
		if (alreadyHandlingUnlikelyEdgeCase) {
			// Prevent infinite loop in case of a bug.
			throw new IllegalStateException("Id generation bug with " + parameterDescription);
		}
		LogUtil.warn("Id generator ran into unlikely edge case:  " +
				parameterDescription + 
				" would return " + Long.toString(badId, radix) +
				" -- resetting counter to 0000 and session prefix to "
				+ Long.toString(newSessionIdPrefix, radix));
		// Normally we would not do this, and probably this code will never be
		// called except for edge-case test.
		SESSION_ID_PREFIX = newSessionIdPrefix;
		nextSequentialSuffix = 0;
		return generateSequentiallyIncreasingIdNumber(newSessionIdPrefix, desiredLength, nDigitsReservedForCounter, radix, true);
	}

	protected synchronized long generateNextSequentialCounter() {
		return nextSequentialSuffix++;
	}	

}
